package com.zhouyer.study.tp;

import com.zhouyer.study.tp.com.zhouyer.study.bean.MethodInfo;
import com.zhouyer.study.tp.com.zhouyer.study.exception.MethodException;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 方法级性能监控
 *
 * @author zhouyao
 * @version 1.0
 * @date 2021/11/22 8:57 下午
 */
// 在服务消费端的方法进行拦截
@Activate(group = CommonConstants.CONSUMER)
public class TPMonitorFilter implements Filter, Runnable {
    /**
     * 记录方法执行的一些信息（方法名，方法执行耗时，方法执行完成时间）
     */
    private Map<String, List<MethodInfo>> methodInfoMap = new ConcurrentHashMap<>();

    public TPMonitorFilter() {
        // 定时线程池，每隔5s执行一次，即每5s打印一次最近1分钟内每个方法的TP90、TP99的耗时情况
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(this, 5, 5, TimeUnit.SECONDS);
    }

    /**
     * 拦截方法执行，记录每个方法的请求耗时
     *
     * @param invoker
     * @param invocation
     */
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Result result = null;
        String methodName = null;
        Long methodTakeTime = 0L;
        // 执行开始时间
        Long startTime = System.currentTimeMillis();
        // 获取方法名称
        methodName = invocation.getMethodName();
        // 执行方法
        result = invoker.invoke(invocation);
        // 处理异常
        if (result.getException() instanceof Exception) {
            throw new MethodException("【" + methodName + "】方法执行失败");
        }
        // 计算方法执行的耗时
        methodTakeTime = System.currentTimeMillis() - startTime;
        // System.out.println(String.format("===============方法【%s】耗时%s毫秒", methodName, methodTakeTime));
        List<MethodInfo> methodInfos = methodInfoMap.computeIfAbsent(methodName, k -> new ArrayList<>());
        // 将当前方法执行的情况添加到map中
        methodInfos.add(new MethodInfo(methodName, methodTakeTime, System.currentTimeMillis()));
        return result;
    }

    /**
     * 该线程方法用于：计算并打印最近1分钟内每个方法的TP90，TP99的耗时情况
     */
    @Override
    public void run() {
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime localDate = LocalDateTime.now();
        String localTime = df.format(localDate);
        System.out.println("=============================Dubbo方法级性能监控（TP90/TP99）==================================");
        // 计算每个方法的TP90/TP99
        for (Map.Entry<String, List<MethodInfo>> entry : methodInfoMap.entrySet()) {
            long tp90 = getMethodTp(entry.getValue(), 0.9);
            long tp99 = getMethodTp(entry.getValue(), 0.99);
            System.out.println(String.format("%s:方法【%s】的TP90：%s,TP99:%s", localTime, entry.getKey(), tp90, tp99));
        }
    }

    /**
     * @param methodInfos 方法执行的情况
     * @param rate        百分比，如TP99就是0.99,TP90就是0.9
     * @return long TP耗时
     * @author zhouyao
     */
    private long getMethodTp(List<MethodInfo> methodInfos, double rate) {
        List<MethodInfo> oneMinuteInMethodInfo = new ArrayList<>();
        // 从当前时间向前推1分钟，即1*60*1000毫秒
        long endTime = System.currentTimeMillis();
        long startTime = endTime - 60000;
        int size = methodInfos.size();
        for (int i = 0; i < size; i++) {
            MethodInfo methodInfo = methodInfos.get(i);
            if (methodInfo.getEndTimes() >= startTime && methodInfo.getEndTimes() <= endTime) {
                oneMinuteInMethodInfo.add(methodInfo);
            }
        }
        // 按照方法的耗时从小到大排序
        oneMinuteInMethodInfo = oneMinuteInMethodInfo.stream().sorted(Comparator.comparingLong(MethodInfo::getTimes)).collect(Collectors.toList());
        // 获取当前排序后集合中指定百分比的位置，该位置就是指定的TP90/TP99
        return oneMinuteInMethodInfo.get((int) (size * rate)).getTimes();
    }
}
